在上一章節有提到,Karman 會用 defineAPI(option)
進行單一支 API 的封裝,並返回一個可以基於 option
內的配置發起請求的函式(FinalAPI
),而這章節會開始提到如何建立一個可以批次管理多支 API 的方法—— defineKarman
。
「卡門樹 Karman Tree」其實顧名思義就是一種樹狀結構,樹上的每個節點會有 0 到 * 個子節點指標,也會存放著該節點的 0 到 * 個 FinalAPI
,還會管理著從祖父節點所繼承下來的共同設定。
在建立 Karman Tree 時,需要使用到 defineKarman
這支 API,這支 API 所返回的物件就是一個節點,排除一些共同設定(headers, interceptors, .etc)以外的參數,defineKarman
會有以下幾個可設定的選項,這些參數通常會影響到最後卡門樹的結構或擁有一些特殊的行為:
root?: boolean
:當前節點是否為根節點。url?: string
:當前節點所管理的 URL 或 URL 片段,有特殊的繼承規則。api?: Record<string, FianlAPI>
:此節點上所封裝的 APIs,為一個物件,key 是 API 名稱,value 是 defineAPI
的返回值 FinalAPI
。route?: Record<string, Karman>
:子節點們,為一個物件,key 是子節點名稱,value 是 defineKarman
的返回值。以下先不以實際可發送請求的 API 演示如何建構卡門樹,以及如何訪問節點及節點上的 FinalAPI
:
import { defineKarman, defineAPI } from "@vic0627/karman"
const karman = defineKarman({
root: true,
url: "...",
api: {
/**
* 根節點上的 Final API
*/
api01: defineAPI({
// ...
})
},
route: {
/**
* 子節點
*/
node01: defineKarman({
url: "...",
api: {
/**
* 子節點上的 Final API
*/
node01api01: defineAPI({
// ...
})
}
})
}
})
const [resPromise01] = karman.api01() // 從根節點訪問根節點上的 Final API
resPromise01.then((res) => console.log(res))
const [resPromise01] = karman.node01.node01api01() // 從根節點訪問子節點,在從子節點訪問子節點上的 Final API
resPromise01.then((res) => console.log(res))
若還記得上一章所介紹的 defineAPI
,會發現它與 defineKarman
一樣都有 url
參數,且一樣都為非必須參數,那麼在 FinalAPI 該怎麼取得請求的 URL 呢?
這部分就必須提到 Karman 的其中的一項特色「路徑管理」,路徑會由父節點往下繼承、拼接,若是 FinalAPI 沒有設定 url
,就會參考它所屬節點的 url
,若是當前的節點並沒有設定 url
,就會參考父層節點的 url
,若是以前端最熟悉的 XML 來舉例節點與 FinalAPI 的路徑的繼承關係:
<karman root url="https://fakestoreapi.com" base-url="https://fakestoreapi.com">
<final-api url="auth/login" base-url="https://fakestoreapi.com/auth/login" />
<karman url="products" base-url="https://fakestoreapi.com/products">
<final-api base-url="https://fakestoreapi.com/products" />
<final-api url=":id" base-url="https://fakestoreapi.com/products/:id" />
<final-api url="categories" base-url="https://fakestoreapi.com/products/categories" />
<!-- and more... -->
</karman>
<karman url="carts" base-url="https://fakestoreapi.com/carts">
<final-api url=":id" base-url="https://fakestoreapi.com/carts/:id" />
<final-api url="../other/path" base-url="https://fakestoreapi.com/other/path" />
<!-- and more... -->
</karman>
</karman>
你會在在最後一個
<final-api />
的url
看到有相對路徑存在,並且 Karman 不但容許其存在,而且是會生效的,這是因為 Karman 在設計之初的目的,就是要能夠給開發人員最大的彈性去管理路由,因為在一些 legacy 的專案中,後端 API 可能不會按照 Restful 風格設計。
若是將上面的 XML 以程式實作,範例如下:
import { defineKarman, defineAPI } from "@vic0627/karman"
export default defineKarman({
root: true,
url: "https://fakestoreapi.com",
api: {
login: defineAPI({
url: "auth/login"
})
},
route: {
product: defineKarman({
url: "products",
api: {
getAll: defineAPI(),
getById: defineAPI({
url: ":id"
}),
getCategories: defineAPI({
url: "categories"
})
}
}),
cart: defineKarman({
url: "carts",
api: {
getById: defineAPI({
url: ":id"
}),
outOfParadigm: defineAPI({
url: "../other/path"
})
}
})
}
})
「繼承事件」是一個會發生在當節點的 root
屬性被設置為 true
時所觸發的事件,事件觸發時,會由根節點開始將可繼承的設定往子節點傳播,若是繼承的設定被子節點給複寫,複寫後的設定將作為新的繼承設定繼續往子孫節點傳播。
卡門樹只能有一個根節點,且必須是頂層的節點,否則 Karman 將拋出錯誤。
FinalAPI 同樣會有繼承,但並不是發生在初始化時,也就是 FinalAPI 的繼承事件不會跟節點的繼承事件同時發生。
比較要注意的事情是,屬性覆寫的行為同樣會發生在物件型別的屬性,這是什麼意思呢?
在 defineKarman
當中會有兩個可繼承的物件屬性 headers
與 auth
,這兩個屬性在繼承時,不是引用物件的址,而是會在子節點上複製一個新的物件,再以子節點的物件進行複寫,因此假設程式碼如下:
// ...
export default defineKarman({
// ...
headers: {
"Content-Type": "application/json",
Token: "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
},
api: {
api01: defineAPI({
// ...
headers: {
Hello: "Karman"
}
})
},
route: {
node01: defineKarman({
headers: {
"Content-Type": "text/plain",
},
api: {
node01api01: defineAPI({
// ...
headers: {
Token: "SSBsb3ZlIEthcm1hbiE="
}
})
}
})
}
})
我們可以得到各個節點與 FinalAPI 的 headers
如下:
root
{
"Content-Type": "application/json",
"Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
}
root.api01
{
"Content-Type": "application/json",
"Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ==",
"Hello": "Karman"
}
root.node01
{
"Content-Type": "text/plain",
"Token": "S2FybWFuIGlzIHRoZSBiZXN0IQ=="
}
root.node01.node01api01
{
"Content-Type": "text/plain",
"Token": "SSBsb3ZlIEthcm1hbiE="
}
在卡門樹中常見的可繼承的設定還有下列這部分,這邊以功能性進行分類,並在 Karman 特有的功能上簡單說明一下:
一般功能性設定:
validation
使否啟用驗證引擎。快取設定:
cache
是否啟用響應快取。cacheExpireTime
快取有效時間。cacheStrategy
快取存放的位置。請求設定:
headers
auth
timeout
timeoutErrorMessage
responseType
headerMap
withCredentials
攔截器:
onRequest
請求發起前呼叫,可以對請求物件進行攔截,寫入動態設定等。onResponse
請求完成時呼叫,可定義請求成功時的條件。在 Karman 中,會有一些特殊的功能或設定只能透過根節點觸發,這些設定或功能通常也不具備繼承的特性。
是的,卡門樹也具備排程任務的機制,但這機制目前只負擔快取的清除任務而已,而排程任務採被動觸發的機制,所以我們只能設定每次執行排程的時間間隔 scheduleInterval
。
當今天排程管理器接收到任務,會把任務加入一個隊列,當隊列內存在一個以上(含)的任務時,就會自動啟用排程任務機制,並在固定的時間點上執行隊列中所有任務,而當有任務完成時,該任務就會從隊列中剃除,直到隊列為空,排程管理器就會自動關閉。
另外,目前 Karman 只有一個排程管理器,意思就是說,當今天存在兩個以上的卡門樹時,這兩個卡門樹會共用同一個排程管理器,並且只會以第二個卡門樹所設定的 scheduleInterval
為準。
後續的章節裡,會陸陸續續提到 Karman 的其中一個功能 Middleware,這些 Middleware 都是函式型別,並且會綁定當前的節點作為 this
所指向的上下文。
通常在這些 Middleware 內,可能會進行一些基本的資料處理,資料處理的共通邏輯就能以外掛的形式註冊到卡門樹上,再透過 Middleware 內的 this
訪問外掛的共通邏輯或常數等,而註冊必須統一由根節點進行,否則 Karman 將會拋出錯誤。
Karman 本身已經有內建的外掛模組,包含以下兩個:
Karman._typeCheck
:包含各種型別驗證的方法,為 Karman 驗證引擎所使用的基本模組。Karman._pathResolver
:為路徑字串的操作模組,本身也被 Karman 廣泛使用。而要在卡門樹上註冊外掛需要使用 Karman.$use(plugins)
這個方法,而外怪本身需要有一個 install(karman)
具體方法,假設我們有一個函式 _add()
要註冊為外掛:
// /karman/plugins/add.js
export default function _add(a, b) {
return a + b
}
// install() 的具體實現
Object.defineProperty(_add, "install", {
value: (karman) => {
Object.defineProperty(karman, "_add", {
value: _add
})
}
})
接下來就能在根節點上嘗試註冊此外掛,並且假設根節點上有一個子節點 product
:
// karman/index.js
import { defineKarman, defineAPI } from "@vic0627/karman"
import product from "./karman/routes/product.js"
import _add from "./plugins/add.js"
const karman = defineKarman({
// ...
route: {
/**
* ## 商品管理 API
*/
product
}
})
// 註冊外掛函式
kaman.$use(_add)
export default karman
後續我們就能在這座卡門樹上的任一 Middleware 用 this
訪問這個外掛:
// karman/routes/product.js
// ...
export default defineKarman({
// ...
onRequest() {
const sum = this._add(2, 4)
console.log(sum)
},
api: {
getAll: defineAPI({
// ...
onSuccess(res) {
const sum = this._add(9, 8)
console.log(sum)
}
})
}
})
透過上述方式註冊好外掛後,的確能從 Middleware 中調用到外掛,但美中不足的是,註冊外掛並沒有支援語法提示,因此可以透過外掛的聲明文件,對 Karman 進行模組聲明,擴展 KarmanDependencies
介面:
// /karman/plugins/add.d.ts
interface Add {
(a: number, b: number): number;
(a: string, b: string): string;
}
export default function _add: Add;
declare module "@vic0627/karman" {
interface KarmanDependencies {
/**
* 相加
*/
_add: Add;
}
}
今天這個章節主要介紹了什麼是卡門樹,以及卡門樹是如何去做路由管理以及卡門樹的各項基本設定,下一個章節會會開始介紹 Karman 的核心功能之一——「參數驗證引擎」。